// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: xguo

#pragma once

#include <memory>
#include <string>

#include <rapidjson/writer.h>

#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/huobi_futures/api_util/auth_key.h"
#include "coin2/exchange/huobi_futures/symbology/product.h"
#include "coin2/exchange/order/huobi.pb.h"

using coin2::exchange::base::symbology::ContractType;
using coin2::exchange::base::symbology::ContractTypeToString;
using coin2::exchange::huobi_futures::symbology::HuobiFuturesProduct;

template <typename Container>
std::string ToList(Container container) {
  std::string res;
  for (auto& v : container) {
    if (res.empty()) {
      res += v;
    } else {
      res += ",";
      res += v;
    }
  }
  return res;
}

inline const char* ToNativeContractType(ContractType contract_type) {
  constexpr static char this_week[] = "this_week";
  constexpr static char next_week[] = "next_week";
  constexpr static char quarter[] = "quarter";
  constexpr static char next_quarter[] = "next_quarter";

  switch (contract_type) {
    case ContractType::WEEK: {
      return this_week;
      break;
    }
    case ContractType::NEXT_WEEK: {
      return next_week;
      break;
    }
    case ContractType::QUARTER: {
      return quarter;
      break;
    }
    case ContractType::NEXT_QUARTER: {
      return next_quarter;
      break;
    }
    default: {
      NOTREACHED() << "contract type not supported! " << ContractTypeToString(contract_type);
    }
  }
  return "";
}

class HuobiFuturesNativePrivateClient : public coin2::exchange::base::order::NativePrivateClient {
 public:
  using RestParams = coin2::exchange::order::HuobiFuturesRestParams;
  using NativeClientConfig = coin2::exchange::order::OrderConnectionConfig;

 public:
  std::shared_ptr<AsioHttpContext> SubmitOrderExecution(const OrderExecutionProto& order) {
    switch (order.type()) {
      case OrderExecutionProto::SUBMIT_ORDER: {
        auto order_spec = OrderSpec::FromProto(order.order_spec());
        auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
        return PlaceSingleOrder(params);
      }
      case OrderExecutionProto::CANCEL_ORDER: {
        auto order_spec = OrderSpec::FromProto(order.order_spec());
        auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
        return CancelOrder(params);
      }
      default: {
        return nullptr;
      }
    }
  }

  std::shared_ptr<AsioHttpContext> SubmitRestQuery(const RestQueryProto& query) {
    RestParams params;
    switch (query.type()) {
      case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
        return QueryAccountInfo(params);
      }
      case RestQueryProto::QUERY_ACCOUNT_POSITION: {
        return QueryPositionInfo(params);
      }
      case RestQueryProto::QUERY_OPEN_ORDERS: {
        return QueryOpenOrders(params);
      }
      default: {
        return nullptr;
      }
    }
  }

 public:
  explicit HuobiFuturesNativePrivateClient(
      const NativeClientConfig& config_override,
      std::string mea_api_str) {
    NativeClientConfig config;
    config.set_rest_host("api.hbdm.com");
    config.set_rest_host_port(443);
    config.MergeFrom(config_override);
    LOG(INFO) << "[HuobiFuturesNativePrivateClient] config: " << config.DebugString();

    auths_.Construct<HuobiRestAuth, NativeClientConfig>(config);

    if (mea_api_str == "v1-linear-swap") {
      if (config.use_cross_mode()) {
        account_path_ = "/linear-swap-api/v1/swap_cross_account_info";
        position_path_ = "/linear-swap-api/v1/swap_cross_position_info";
        open_orders_path_ = "/linear-swap-api/v1/swap_cross_openorders";
        query_fill_path_ = "/linear-swap-api/v3/swap_cross_matchresults";
        history_orders_path_ = "/linear-swap-api/v3/swap_cross_hisorders";
        query_transfer_path_ = "/linear-swap-api/v3/swap_financial_record";
        place_single_order_path_ = "/linear-swap-api/v1/swap_cross_order";
        place_batch_orders_path_ = "/linear-swap-api/v1/swap_cross_batchorder";
        cancel_order_path_ = "/linear-swap-api/v1/swap_cross_cancel";
        cancel_all_order_path_ = "/linear-swap-api/v1/swap_cross_cancelall";
      } else {
        account_path_ = "/linear-swap-api/v1/swap_account_info";
        position_path_ = "/linear-swap-api/v1/swap_position_info";
        open_orders_path_ = "/linear-swap-api/v1/swap_openorders";
        query_fill_path_ = "/linear-swap-api/v3/swap_matchresults";
        history_orders_path_ = "/linear-swap-api/v3/swap_hisorders";
        query_transfer_path_ = "/linear-swap-api/v3/swap_financial_record";
        place_single_order_path_ = "/linear-swap-api/v1/swap_order";
        place_batch_orders_path_ = "/linear-swap-api/v1/swap_batchorder";
        cancel_order_path_ = "/linear-swap-api/v1/swap_cancel";
        cancel_all_order_path_ = "/linear-swap-api/v1/swap_cancelall";
      }
      is_swap_ = true;
    } else if (mea_api_str == "v1-swap") {
      account_path_ = "/swap-api/v1/swap_account_info";
      position_path_ = "/swap-api/v1/swap_position_info";
      open_orders_path_ = "/swap-api/v1/swap_openorders";
      query_fill_path_ = "/swap-api/v3/swap_matchresults";
      history_orders_path_ = "/swap-api/v3/swap_hisorders";
      query_transfer_path_ = "/swap-api/v3/swap_financial_record";
      place_single_order_path_ = "/swap-api/v1/swap_order";
      place_batch_orders_path_ = "/swap-api/v1/swap_batchorder";
      cancel_order_path_ = "/swap-api/v1/swap_cancel";
      cancel_all_order_path_ = "/swap-api/v1/swap_cancelall";
      is_swap_ = true;
    } else {
      account_path_ = "/api/v1/contract_account_info";
      position_path_ = "/api/v1/contract_position_info";
      open_orders_path_ = "/api/v1/contract_openorders";
      query_fill_path_ = "/api/v3/contract_matchresults";
      history_orders_path_ = "/api/v3/contract_hisorders";
      query_transfer_path_ = "/api/v3/contract_financial_record";
      place_single_order_path_ = "/api/v1/contract_order";
      place_batch_orders_path_ = "/api/v1/contract_batchorder";
      cancel_order_path_ = "/api/v1/contract_cancel";
      cancel_all_order_path_ = "/api/v1/contract_cancelall";
      is_swap_ = false;
    }
  }

  std::shared_ptr<AsioHttpContext> QueryAccountInfo(const RestParams& params) {
    UrlParamsBuilder builder;
    CHECK(!params.has_symbol()) << "NEVER use per currency query for hbdm."
                                << " ask donggu, jhkim, taekwon for this.";
    auto* context = GetAuth()->CreateRequestByPostWithSignature(account_path_, &builder);

    context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryAccountNonCrossBalance(const RestParams& params) {
    CHECK(!params.has_symbol()) << "NEVER use per currency query for hbdm."
                                << " ask donggu, jhkim, taekwon for this.";
    UrlParamsBuilder builder;
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/linear-swap-api/v1/swap_account_info",
        &builder);
    context->query_type = OrderSubTopicId::REST_ACCOUNT_NON_CROSS_BALANCE;

    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryAccountCrossBalance(const RestParams& params) {
    CHECK(!params.has_symbol()) << "NEVER use per currency query for hbdm."
                                << " ask donggu, jhkim, taekwon for this.";
    UrlParamsBuilder builder;
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/linear-swap-api/v1/swap_cross_account_info",
        &builder);
    context->query_type = OrderSubTopicId::REST_ACCOUNT_CROSS_BALANCE;

    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryPositionInfo(const RestParams& params) {
    UrlParamsBuilder builder;
    SetParamsSymbol(params, &builder);
    auto* context = GetAuth()->CreateRequestByPostWithSignature(position_path_, &builder);

    context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryAccountNonCrossPosition(const RestParams& params) {
    UrlParamsBuilder builder;
    SetParamsSymbol(params, &builder);
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/linear-swap-api/v1/swap_position_info",
        &builder);

    context->query_type = OrderSubTopicId::REST_ACCOUNT_NON_CROSS_POSITION;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryAccountCrossPosition(const RestParams& params) {
    UrlParamsBuilder builder;
    SetParamsSymbol(params, &builder);
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/linear-swap-api/v1/swap_cross_position_info",
        &builder);

    context->query_type = OrderSubTopicId::REST_ACCOUNT_CROSS_POSITION;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryOpenOrders(const RestParams& params) {
    UrlParamsBuilder builder;
    SetParamsSymbol(params, &builder);
    auto* context = GetAuth()->CreateRequestByPostWithSignature(open_orders_path_, &builder);

    context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryFills(const RestParams& params) {
    UrlParamsBuilder builder;
    SetParamsSymbol(params, &builder, true);
    builder.PutPost("trade_type", params.trade_type());
    builder.PutPost("direct", "prev");
    builder.PutPost("start_time", GetCurrentTimestampMs() - 24 * 60 * 60 * 1000);
    auto* context = GetAuth()->CreateRequestByPostWithSignature(query_fill_path_, &builder);

    context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryHistoryOrders(const RestParams& params) {
    UrlParamsBuilder builder;
    SetParamsSymbol(params, &builder, true);
    builder.PutPost("trade_type", params.trade_type());
    builder.PutPost("type", params.type());
    builder.PutPost("status", params.status());
    builder.PutPost("direct", "prev");
    builder.PutPost("start_time", GetCurrentTimestampMs() - 24 * 60 * 60 * 1000);
    auto* context = GetAuth()->CreateRequestByPostWithSignature(history_orders_path_, &builder);

    context->query_type = OrderSubTopicId::REST_CANCEL_ORDERS;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> QueryTransfer(const RestParams& params) {
    UrlParamsBuilder builder;
    SetParamsSymbol(params, &builder, true);
    builder.PutPost("direct", "prev");
    auto* context = GetAuth()->CreateRequestByPostWithSignature(query_transfer_path_, &builder);

    context->query_type = OrderSubTopicId::TRANSFER;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> PlaceSingleOrder(const RestParams& params) {
    auto builder = GetOrderParamBuilder(params);
    auto* context = GetAuth()->CreateRequestByPostWithSignature(place_single_order_path_, &builder);

    context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> PlaceBatchOrders(const std::vector<RestParams>& params) {
    rapidjson::StringBuffer str_buf;
    rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
    writer.StartObject();

    writer.Key("orders_data");
    writer.StartArray();

    for (const auto& param : params) {
      writer.StartObject();

      if (!is_swap_) {
        writer.Key("symbol");
        writer.String(param.symbol().c_str());
        writer.Key("contract_type");
        writer.String(param.contract_type().c_str());
      }

      writer.Key("contract_code");
      writer.String(param.contract_code().c_str());
      writer.Key("client_order_id");
      writer.Int64(param.client_order_id());
      writer.Key("price");
      writer.String(param.price().c_str());
      writer.Key("volume");
      writer.Int64(param.volume());
      writer.Key("direction");
      writer.String(param.direction().c_str());
      writer.Key("offset");
      writer.String(param.offset().c_str());
      writer.Key("lever_rate");
      writer.Int64(param.lever_rate());
      writer.Key("order_price_type");
      writer.String(param.order_price_type().c_str());

      writer.EndObject();
    }

    writer.EndArray();

    writer.EndObject();

    auto* context = static_cast<HuobiRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
        place_batch_orders_path_,
        str_buf.GetString());

    context->query_type = OrderSubTopicId::MULTI_SUBMIT_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> CancelOrder(const RestParams& params) {
    UrlParamsBuilder builder;

    SetParamsSymbol(params, &builder);
    CHECK((params.order_id_list_size() > 0) != (params.client_order_id_list_size() > 0))
        << "only one of order_id and client_order_id can be set!" << params.DebugString();

    if (params.order_id_list_size() > 0) {
      builder.PutPost("order_id", ToList(params.order_id_list()));
    } else {
      builder.PutPost("client_order_id", ToList(params.client_order_id_list()));
    }

    auto* context = GetAuth()->CreateRequestByPostWithSignature(cancel_order_path_, &builder);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }

  std::shared_ptr<AsioHttpContext> CancelProductOrders(const RestParams& params) {
    UrlParamsBuilder builder;

    SetParamsSymbol(params, &builder);
    if (params.has_contract_code()) {
      builder.PutPost("contract_code", params.contract_code());
    }
    if (params.has_contract_type()) {
      builder.PutPost("contract_type", params.contract_type());
    }

    auto* context = GetAuth()->CreateRequestByPostWithSignature(cancel_all_order_path_, &builder);
    return std::shared_ptr<AsioHttpContext>(context);
  }

  RestParams GetRestOrderParams(const OrderSpec& order_spec, int64_t proc_order_id) {
    auto params = RestParams();
    auto product = HuobiFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    params.set_price(formatter.FormatPrice(order_spec.price));
    params.set_volume(order_spec.qty);
    params.set_client_order_id(proc_order_id);

    if (!is_swap_) {
      params.set_contract_type(ToNativeContractType(product.contract_type()));
    }
    params.set_contract_code(order_spec.product);

    if (order_spec.order_side == OrderSide::BUY_OPEN_ORDER) {
      params.set_direction("buy");
      params.set_offset("open");
    } else if (order_spec.order_side == OrderSide::SELL_OPEN_ORDER) {
      params.set_direction("sell");
      params.set_offset("open");
    } else if (order_spec.order_side == OrderSide::BUY_CLOSE_ORDER) {
      params.set_direction("buy");
      params.set_offset("close");
    } else if (order_spec.order_side == OrderSide::SELL_CLOSE_ORDER) {
      params.set_direction("sell");
      params.set_offset("close");
    } else {
      NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
    }
    params.set_lever_rate(order_spec.leverage_rate);

    if (order_spec.post_only) {
      params.set_order_price_type("post_only");
    } else if (order_spec.duration == coin::proto::OrderDuration::GTC_ORDER) {
      params.set_order_price_type("limit");
    } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
      params.set_order_price_type("ioc");
    }

    return params;
  }

  RestParams GetRestCancelParams(const IOrderContext& order_context) {
    const auto& order_spec = order_context.order_spec();
    HuobiFuturesProduct product =
        HuobiFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto params = RestParams();
    if (!is_swap_) {
      params.set_symbol(StringToUpperCopy(product.base().native_currency()));
    } else {
      params.set_contract_code(product.order_native_symbol());
    }
    if (order_context.has_exchange_order_id()) {
      params.add_order_id_list(order_context.exchange_order_id());
      CHECK_EQ(params.order_id_list_size(), 1);
    } else {
      params.add_client_order_id_list(std::to_string(order_context.proc_order_id()));
      CHECK_EQ(params.client_order_id_list_size(), 1);
    }
    return params;
  }

 protected:
  std::string account_path_;
  std::string position_path_;
  std::string open_orders_path_;
  std::string query_fill_path_;
  std::string history_orders_path_;
  std::string query_transfer_path_;
  std::string place_single_order_path_;
  std::string place_batch_orders_path_;
  std::string cancel_order_path_;
  std::string cancel_all_order_path_;
  bool is_swap_;

 private:
  UrlParamsBuilder GetOrderParamBuilder(const RestParams& params) const {
    UrlParamsBuilder builder;
    if (!is_swap_) {
      builder.PutPost("symbol", params.symbol());
      builder.PutPost("contract_type", params.contract_type());
    }
    builder.PutPost("contract_code", params.contract_code());
    builder.PutPost("client_order_id", params.client_order_id());
    builder.PutPost("price", params.price());
    builder.PutPost("volume", params.volume());
    builder.PutPost("direction", params.direction());
    builder.PutPost("offset", params.offset());
    builder.PutPost("lever_rate", params.lever_rate());
    builder.PutPost("order_price_type", params.order_price_type());
    return builder;
  }

  void SetParamsSymbol(const RestParams& params, UrlParamsBuilder* builder, bool is_v3 = false)
      const {
    if (is_swap_) {
      if (params.has_contract_code()) {
        if (is_v3) {
          builder->PutPost("contract", params.contract_code());
        } else {
          builder->PutPost("contract_code", params.contract_code());
        }
      }
    } else {
      if (params.has_symbol()) {
        builder->PutPost("symbol", params.symbol());
      }
    }
  }

 public:
  std::shared_ptr<AsioHttpContext> PlaceOrderNorm(const OrderExecution& order_exe) {
    if (!order_exe.is_place_batch_orders) {
      const auto& order_spec = *order_exe.order_spec;
      const auto proc_order_id = order_exe.proc_order_id;
      auto params = GetRestOrderParams(order_spec, proc_order_id);
      return PlaceSingleOrder(params);
    }

    auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

    if (mutable_order_exe->place_pending_order_deque.empty()) {
      return {};
    }

    std::vector<RestParams> params;
    for (int i = 0; i < 10 && !mutable_order_exe->place_pending_order_deque.empty(); ++i) {
      const auto* order_spec = mutable_order_exe->place_pending_order_deque.front();
      mutable_order_exe->place_pending_order_deque.pop_front();

      mutable_order_exe->place_proceed_order_deque.push_back(order_spec);

      params.emplace_back(GetRestOrderParams(*order_spec, order_spec->proc_order_id));
    }

    return PlaceBatchOrders(params);
  }

  std::shared_ptr<AsioHttpContext> CancelOrderNorm(const OrderExecution& order_exe) {
    auto params = GetRestCancelParams(*order_exe.oc);
    return CancelOrder(params);
  }
};
